/*
 * Copyright (c) 2009-2021 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.scene;

import com.jme3.asset.AssetNotFoundException;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.material.Material;
import com.jme3.math.Matrix4f;
import com.jme3.renderer.Camera;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.scene.mesh.MorphTarget;
import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
import com.jme3.util.TempVars;
import com.jme3.util.clone.Cloner;
import com.jme3.util.clone.IdentityCloneFunction;
import java.io.IOException;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <code>Geometry</code> defines a leaf node of the scene graph. The leaf node
 * contains the geometric data for rendering objects. It manages all rendering
 * information such as a {@link Material} object to define how the surface
 * should be shaded and the {@link Mesh} data to contain the actual geometry.
 *
 * @author Kirill Vainer
 */
public class Geometry extends Spatial {
    // Version #1: removed shared meshes.
    // models loaded with shared mesh will be automatically fixed.
    public static final int SAVABLE_VERSION = 1;
    private static final Logger logger = Logger.getLogger(Geometry.class.getName());
    protected Mesh mesh;
    protected transient int lodLevel = 0;
    protected Material material;
    /**
     * When true, the geometry's transform will not be applied.
     */
    protected boolean ignoreTransform = false;
    protected transient Matrix4f cachedWorldMat = new Matrix4f();
    /**
     * Specifies which {@link GeometryGroupNode} this <code>Geometry</code>
     * is managed by.
     */
    protected GeometryGroupNode groupNode;
    /**
     * The start index of this <code>Geometry's</code> inside
     * the {@link GeometryGroupNode}.
     */
    protected int startIndex = -1;
    /**
     * Morph state variable for morph animation
     */
    private float[] morphState;
    private boolean dirtyMorph = true;
    // a Morph target that will be used to merge all targets that
    // can't be handled on the cpu on each frame.
    private MorphTarget fallbackMorphTarget;
    private int nbSimultaneousGPUMorph = -1;

    /**
     * Instantiate a <code>Geometry</code> with no name, no mesh, and no
     * material. The mesh and material must be set prior to rendering.
     */
    public Geometry() {
        this(null);
    }

    /**
     * Create a geometry node without any mesh data.
     * Both the mesh and the material are null, the geometry
     * cannot be rendered until those are set.
     *
     * @param name The name of this geometry
     */
    public Geometry(String name) {
        super(name);

        // For backwards compatibility, only clear the "requires
        // update" flag if we are not a subclass of Geometry.
        // This prevents subclass from silently failing to receive
        // updates when they upgrade.
        setRequiresUpdates(Geometry.class != getClass());
    }

    /**
     * Create a geometry node with mesh data.
     * The material of the geometry is null, it cannot
     * be rendered until it is set.
     *
     * @param name The name of this geometry
     * @param mesh The mesh data for this geometry
     */
    public Geometry(String name, Mesh mesh) {
        this(name);

        if (mesh == null) {
            throw new IllegalArgumentException("mesh cannot be null");
        }

        this.mesh = mesh;
    }

    /**
     * Create a geometry node with mesh data and material.
     *
     * @param name The name of this geometry
     * @param mesh The mesh data for this geometry
     * @param material The material for this geometry
     */
    public Geometry(String name, Mesh mesh, Material material) {
        this(name, mesh);
        setMaterial(material);
    }

    @Override
    public boolean checkCulling(Camera cam) {
        if (isGrouped()) {
            setLastFrustumIntersection(Camera.FrustumIntersect.Outside);
            return false;
        }
        return super.checkCulling(cam);
    }

    /**
     * @return If ignoreTransform mode is set.
     *
     * @see Geometry#setIgnoreTransform(boolean)
     */
    public boolean isIgnoreTransform() {
        return ignoreTransform;
    }

    /**
     * @param ignoreTransform If true, the geometry's transform will not be applied.
     */
    public void setIgnoreTransform(boolean ignoreTransform) {
        this.ignoreTransform = ignoreTransform;
    }

    /**
     * Sets the LOD level to use when rendering the mesh of this geometry.
     * Level 0 indicates that the default index buffer should be used,
     * levels [1, LodLevels + 1] represent the levels set on the mesh
     * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }.
     *
     * @param lod The lod level to set
     */
    @Override
    public void setLodLevel(int lod) {
        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
        if (mesh.getNumLodLevels() == 0) {
            throw new IllegalStateException("LOD levels are not set on this mesh");
        }

        if (lod < 0 || lod >= mesh.getNumLodLevels()) {
            throw new IllegalArgumentException("LOD level is out of range: " + lod);
        }

        lodLevel = lod;

        if (isGrouped()) {
            groupNode.onMeshChange(this);
        }
    }

    /**
     * Returns the LOD level set with {@link #setLodLevel(int) }.
     *
     * @return the LOD level set
     */
    public int getLodLevel() {
        return lodLevel;
    }

    /**
     * Returns this geometry's mesh vertex count.
     *
     * @return this geometry's mesh vertex count.
     *
     * @see Mesh#getVertexCount()
     */
    @Override
    public int getVertexCount() {
        return mesh.getVertexCount();
    }

    /**
     * Returns this geometry's mesh triangle count.
     *
     * @return this geometry's mesh triangle count.
     *
     * @see Mesh#getTriangleCount()
     */
    @Override
    public int getTriangleCount() {
        return mesh.getTriangleCount();
    }

    /**
     * Sets the mesh to use for this geometry when rendering.
     *
     * @param mesh the mesh to use for this geometry
     *
     * @throws IllegalArgumentException If mesh is null
     */
    public void setMesh(Mesh mesh) {
        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
        if (mesh == null) {
            throw new IllegalArgumentException();
        }

        this.mesh = mesh;
        setBoundRefresh();

        if (isGrouped()) {
            groupNode.onMeshChange(this);
        }
    }

    /**
     * Returns the mesh to use for this geometry
     *
     * @return the mesh to use for this geometry
     *
     * @see #setMesh(com.jme3.scene.Mesh)
     */
    public Mesh getMesh() {
        return mesh;
    }

    /**
     * Sets the material to use for this geometry.
     *
     * @param material the material to use for this geometry
     */
    @Override
    public void setMaterial(Material material) {
        assert SceneGraphThreadWarden.assertOnCorrectThread(this);
        this.material = material;
        nbSimultaneousGPUMorph = -1;
        if (isGrouped()) {
            groupNode.onMaterialChange(this);
        }
    }

    /**
     * Returns the material that is used for this geometry.
     *
     * @return the material that is used for this geometry
     *
     * @see #setMaterial(com.jme3.material.Material)
     */
    public Material getMaterial() {
        return material;
    }

    /**
     * @return The bounding volume of the mesh, in model space.
     */
    public BoundingVolume getModelBound() {
        return mesh.getBound();
    }

    /**
     * Updates the bounding volume of the mesh. Should be called when the
     * mesh has been modified.
     */
    @Override
    public void updateModelBound() {
        mesh.updateBound();
        setBoundRefresh();
    }

    /**
     * <code>updateWorldBound</code> updates the bounding volume that contains
     * this geometry. The location of the geometry is based on the location of
     * all this node's parents.
     *
     * @see Spatial#updateWorldBound()
     */
    @Override
    protected void updateWorldBound() {
        super.updateWorldBound();
        if (mesh == null) {
            throw new IllegalStateException("Geometry \"" + getName() + "\" has null mesh.");
        }

        if (mesh.getBound() != null) {
            if (ignoreTransform) {
                // we do not transform the model bound by the world transform,
                // just use the model bound as-is
                worldBound = mesh.getBound().clone(worldBound);
            } else {
                worldBound = mesh.getBound().transform(worldTransform, worldBound);
            }
        }
    }

    @Override
    protected void updateWorldTransforms() {
        super.updateWorldTransforms();
        computeWorldMatrix();

        if (isGrouped()) {
            groupNode.onTransformChange(this);
        }

        // geometry requires lights to be sorted
        worldLights.sort(true);
    }

    @Override
    protected void updateWorldLightList() {
        super.updateWorldLightList();
        // geometry requires lights to be sorted
        worldLights.sort(true);
    }

    /**
     * Associate this <code>Geometry</code> with a {@link GeometryGroupNode}.
     *
     * Should only be called by the parent {@link GeometryGroupNode}.
     *
     * @param node Which {@link GeometryGroupNode} to associate with.
     * @param startIndex The starting index of this geometry in the group.
     */
    public void associateWithGroupNode(GeometryGroupNode node, int startIndex) {
        if (isGrouped()) {
            unassociateFromGroupNode();
        }

        this.groupNode = node;
        this.startIndex = startIndex;
    }

    /**
     * Removes the {@link GeometryGroupNode} association from this
     * <code>Geometry</code>.
     *
     * Should only be called by the parent {@link GeometryGroupNode}.
     */
    public void unassociateFromGroupNode() {
        if (groupNode != null) {
            // Once the geometry is removed
            // from the parent, the group node needs to be updated.
            groupNode.onGeometryUnassociated(this);
            groupNode = null;

            // change the default to -1 to make error detection easier
            startIndex = -1;
        }
    }

    @Override
    public boolean removeFromParent() {
        return super.removeFromParent();
    }

    @Override
    protected void setParent(Node parent) {
        super.setParent(parent);

        // If the geometry is managed by group node we need to unassociate.
        if (parent == null && isGrouped()) {
            unassociateFromGroupNode();
        }
    }

    /*
     * Indicate that the transform of this spatial has changed and that
     * a refresh is required.
     */
    // NOTE: Spatial has an identical implementation of this method,
    // thus it was commented out.
//    @Override
//    protected void setTransformRefresh() {
//        refreshFlags |= RF_TRANSFORM;
//        setBoundRefresh();
//    }
    /**
     * Recomputes the matrix returned by {@link Geometry#getWorldMatrix() }.
     * This will require a localized transform update for this geometry.
     */
    public void computeWorldMatrix() {
        // Force a local update of the geometry's transform
        checkDoTransformUpdate();

        // Compute the cached world matrix
        cachedWorldMat.loadIdentity();
        if (ignoreTransform) {
            return;
        }
        cachedWorldMat.setRotationQuaternion(worldTransform.getRotation());
        cachedWorldMat.setTranslation(worldTransform.getTranslation());

        TempVars vars = TempVars.get();
        Matrix4f scaleMat = vars.tempMat4;
        scaleMat.loadIdentity();
        scaleMat.scale(worldTransform.getScale());
        cachedWorldMat.multLocal(scaleMat);
        vars.release();
    }

    /**
     * A {@link Matrix4f matrix} that transforms the {@link Geometry#getMesh() mesh}
     * from model space to world space. This matrix is computed based on the
     * {@link Geometry#getWorldTransform() world transform} of this geometry.
     * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() }
     * before using this method.
     *
     * @return Matrix to transform from local space to world space
     */
    public Matrix4f getWorldMatrix() {
        return cachedWorldMat;
    }

    /**
     * Sets the model bound to use for this geometry.
     * This alters the bound used on the mesh as well via
     * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and
     * forces the world bounding volume to be recomputed.
     *
     * @param modelBound The model bound to set
     */
    @Override
    public void setModelBound(BoundingVolume modelBound) {
        this.worldBound = null;
        mesh.setBound(modelBound);
        setBoundRefresh();

        // NOTE: Calling updateModelBound() would cause the mesh
        // to recompute the bound based on the geometry thus making
        // this call useless!
        //updateModelBound();
    }

    @Override
    public int collideWith(Collidable other, CollisionResults results) {
        // Force bound to update
        checkDoBoundUpdate();
        // Update transform, and compute cached world matrix
        computeWorldMatrix();

        assert (refreshFlags & (RF_BOUND | RF_TRANSFORM)) == 0;

        if (mesh != null) {
            // NOTE: BIHTree in mesh already checks collision with the
            // mesh's bound
            int prevSize = results.size();
            int added = mesh.collideWith(other, cachedWorldMat, worldBound, results);
            int newSize = results.size();
            for (int i = prevSize; i < newSize; i++) {
                results.getCollisionDirect(i).setGeometry(this);
            }
            return added;
        }
        return 0;
    }

    @Override
    public void depthFirstTraversal(SceneGraphVisitor visitor, DFSMode mode) {
        visitor.visit(this);
    }

    @Override
    protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
    }

    /**
     * Determine whether this <code>Geometry</code> is managed by a
     * {@link GeometryGroupNode} or not.
     *
     * @return True if managed by a {@link GeometryGroupNode}.
     */
    public boolean isGrouped() {
        return groupNode != null;
    }

    /**
     * @deprecated Use {@link #isGrouped()} instead.
     * @return true if managed by a {@link GeometryGroupNode}
     */
    @Deprecated
    public boolean isBatched() {
        return isGrouped();
    }

    /**
     * This version of clone is a shallow clone, in other words, the
     * same mesh is referenced as the original geometry.
     * Exception: if the mesh is marked as being a software
     * animated mesh, (bind pose is set) then the positions
     * and normals are deep copied.
     */
    @Override
    public Geometry clone(boolean cloneMaterial) {
        return (Geometry) super.clone(cloneMaterial);
    }

    /**
     * This version of clone is a shallow clone, in other words, the
     * same mesh is referenced as the original geometry.
     * Exception: if the mesh is marked as being a software
     * animated mesh, (bind pose is set) then the positions
     * and normals are deep copied.
     */
    @Override
    public Geometry clone() {
        return clone(true);
    }

    /**
     * Create a deep clone of the geometry. This creates an identical copy of
     * the mesh with the vertex buffer data duplicated.
     */
    @Override
    public Spatial deepClone() {
        return super.deepClone();
    }

    public Spatial oldDeepClone() {
        Geometry geomClone = clone(true);
        geomClone.mesh = mesh.deepClone();
        return geomClone;
    }

    /**
     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
     */
    @Override
    public void cloneFields(Cloner cloner, Object original) {
        super.cloneFields(cloner, original);

        // If this is a grouped node and if our group node is
        // also cloned then we'll grab its reference.
        if (groupNode != null) {
            if (cloner.isCloned(groupNode)) {
                // Then resolve the reference
                this.groupNode = cloner.clone(groupNode);
            } else {
                // We are on our own now
                this.groupNode = null;
                this.startIndex = -1;
            }

            // The above is based on the fact that if we were
            // cloning the hierarchy that contained the parent
            // group then it would have been shallow cloned before
            // this child.  Can't really be otherwise.
        }

        this.cachedWorldMat = cloner.clone(cachedWorldMat);

        // See if we are doing a shallow clone or a deep mesh clone
        boolean shallowClone = (cloner.getCloneFunction(Mesh.class) instanceof IdentityCloneFunction);

        // See if we clone the mesh using the special animation
        // semi-deep cloning
        if (shallowClone && mesh != null && mesh.getBuffer(Type.BindPosePosition) != null) {
            // Then we need to clone the mesh a little deeper
            this.mesh = mesh.cloneForAnim();
        } else {
            // Do whatever the cloner wants to do about it
            this.mesh = cloner.clone(mesh);
        }

        this.material = cloner.clone(material);
    }

    public void setMorphState(float[] state) {
        if (mesh == null || mesh.getMorphTargets().length == 0) {
            return;
        }

        int nbMorphTargets = mesh.getMorphTargets().length;

        if (morphState == null) {
            morphState = new float[nbMorphTargets];
        }
        System.arraycopy(state, 0, morphState, 0, morphState.length);
        this.dirtyMorph = true;
    }

    /**
     * Set the state of the morph with the given name.
     *
     * If the name of the morph is not found, no state will be set.
     *
     * @param morphTarget The name of the morph to set the state of
     * @param state The state to set the morph to
     */
    public void setMorphState(String morphTarget, float state) {
        int index = mesh.getMorphIndex(morphTarget);
        if (index >= 0) {
            morphState[index] = state;
            this.dirtyMorph = true;
        }
    }

    /**
     * returns true if the morph state has changed on the last frame.
     *
     * @return true if changed, otherwise false
     */
    public boolean isDirtyMorph() {
        return dirtyMorph;
    }

    /**
     * Setting this to true will stop this geometry morph buffer to be updated,
     * unless the morph state changes
     *
     * @param dirtyMorph true&rarr;prevent updating, false&rarr;allow updating
     */
    public void setDirtyMorph(boolean dirtyMorph) {
        this.dirtyMorph = dirtyMorph;
    }

    /**
     * returns the morph state of this Geometry.
     * Used internally by the MorphControl.
     *
     * @return an array
     */
    public float[] getMorphState() {
        if (morphState == null) {
            morphState = new float[mesh.getMorphTargets().length];
        }
        return morphState;
    }

    /**
     * Get the state of a morph
     *
     * @param morphTarget the name of the morph to get the state of
     * @return the state of the morph, or -1 if the morph is not found
     */
    public float getMorphState(String morphTarget) {
        int index = mesh.getMorphIndex(morphTarget);
        if (index < 0) {
            return -1;
        } else {
            return morphState[index];
        }
    }

    /**
     * Return the number of morph targets that can be handled
     * on the GPU simultaneously for this geometry.
     * Note that it depends on the material set on this geometry.
     * This number is computed and set by the MorphControl,
     * so it might be available only after the first frame.
     * Else it's set to -1.
     *
     * @return the number of simultaneous morph targets handled on the GPU
     */
    public int getNbSimultaneousGPUMorph() {
        return nbSimultaneousGPUMorph;
    }

    /**
     * Sets the number of morph targets that can be handled
     * on the GPU simultaneously for this geometry.
     * Note that it depends on the material set on this geometry.
     * This number is computed and set by the MorphControl,
     * so it might be available only after the first frame.
     * Else it's set to -1.
     * WARNING: setting this manually might crash the shader compilation if set too high.
     * Do it at your own risk.
     *
     * @param nbSimultaneousGPUMorph the number of simultaneous morph targets to be handled on the GPU.
     */
    public void setNbSimultaneousGPUMorph(int nbSimultaneousGPUMorph) {
        this.nbSimultaneousGPUMorph = nbSimultaneousGPUMorph;
    }

    public MorphTarget getFallbackMorphTarget() {
        return fallbackMorphTarget;
    }

    public void setFallbackMorphTarget(MorphTarget fallbackMorphTarget) {
        this.fallbackMorphTarget = fallbackMorphTarget;
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        super.write(ex);
        OutputCapsule oc = ex.getCapsule(this);
        oc.write(mesh, "mesh", null);
        if (material != null) {
            oc.write(material.getAssetName(), "materialName", null);
        }
        oc.write(material, "material", null);
        oc.write(ignoreTransform, "ignoreTransform", false);
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        super.read(im);
        InputCapsule ic = im.getCapsule(this);
        mesh = (Mesh) ic.readSavable("mesh", null);

        material = null;
        String matName = ic.readString("materialName", null);
        if (matName != null) {
            // Material name is set,
            // Attempt to load material via J3M
            try {
                material = im.getAssetManager().loadMaterial(matName);
            } catch (AssetNotFoundException ex) {
                // Cannot find J3M file.
                if (logger.isLoggable(Level.FINE)) {
                    logger.log(Level.FINE, "Cannot locate {0} for geometry {1}",
                            new Object[]{matName, key});
                }
            }
        }
        // If material is NULL, try to load it from the geometry
        if (material == null) {
            material = (Material) ic.readSavable("material", null);
        }
        ignoreTransform = ic.readBoolean("ignoreTransform", false);

        if (ic.getSavableVersion(Geometry.class) == 0) {
            // Fix shared mesh (if set)
            Mesh sharedMesh = getUserData(UserData.JME_SHAREDMESH);
            if (sharedMesh != null) {
                getMesh().extractVertexData(sharedMesh);
                setUserData(UserData.JME_SHAREDMESH, null);
            }
        }
    }
}
